Skip to content

feat(chrome): add native browser debugging tools - #7027

Draft
yiliang114 wants to merge 24 commits into
QwenLM:mainfrom
yiliang114:cx/chrome-extension-native-tools
Draft

yiliang114 wants to merge 24 commits into
QwenLM:mainfrom
yiliang114:cx/chrome-extension-native-tools

Conversation

@yiliang114

Copy link
Copy Markdown
Collaborator

What this PR does

This PR gives the official Qwen Code Chrome extension a native browser-debugging tool catalog implemented with chrome.debugger. After a user starts qwen serve and completes first-use pairing, the agent can inspect the accessibility tree, navigate, click, fill forms, send keys, capture screenshots, evaluate JavaScript, read console output, and inspect network requests and opt-in response bodies.

The extension registers these tools through the daemon's reverse MCP channel. The official extension origin and reverse channels work by default, while custom extension builds still require an explicit allowed origin. A process-scoped mutual-proof pairing flow prevents an unrelated local process from claiming the extension or Chrome sessions, and internal CDP compatibility endpoints use an unguessable process token.

The existing external adapter command remains as a compatibility path. Neither the extension ZIP nor the main Qwen Code npm package bundles chrome-devtools-mcp, Puppeteer, or another external browser automation server. Release packaging rejects stale custom build output and scans the generated extension artifact.

Why it's needed

The previous browser automation path depended on separately installing and configuring an external Chrome DevTools MCP adapter. Bundling that adapter into the main npm package also created security-scanner and release-compliance concerns. Hosting the baseline tools in the Chrome extension uses the browser permission the extension already owns, removes the extra installation step, and keeps the Node.js agent runtime in qwen serve.

Reviewer Test Plan

How to verify

  1. Install the generated extension ZIP or load the production extension directory unpacked, then start qwen serve with no browser-related environment variables or origin flags. Confirm that the side panel discovers the daemon and asks for the pairing code printed in the terminal.
  2. Complete pairing and open a normal HTTP(S) page. Confirm that the Web Shell loads and the agent can take a page snapshot and screenshot, click and fill an element, evaluate a small script, and list console and network entries.
  3. Request a network response body and confirm it is returned only when explicitly requested, with authorization, cookies, token-like fields, and URL credentials redacted.
  4. Switch the active tab and confirm the next tool call attaches to the new allowed tab. Open a restricted Chrome page and confirm browser tools reject it.
  5. Restart the daemon and confirm the previous process credential is rejected and the extension requests a new pairing code. Confirm an unpaired ACP client cannot create or restore a Chrome extension session or claim the CDP bridge.
  6. Set an explicit external adapter command and confirm the native catalog stays disabled while the compatibility adapter owns browser automation.

Evidence (Before & After)

Before: browser debugging required a separately installed adapter and explicit environment configuration.

After: the official extension needs only qwen serve plus first-use pairing; the extension-hosted catalog becomes available immediately after pairing.

Automated evidence: 61 extension tests, extension typecheck/build/package/artifact scan, targeted CLI/ACP/SDK/Web UI/Web Shell tests, full workspace typecheck/lint/build, and a real npm tarball content scan all pass. The npm tarball contains 833 files and no Chrome extension source, extension ZIP, or external Chrome DevTools MCP implementation.

Tested on

OS Status
🍏 macOS
🪟 Windows N/A
🐧 Linux N/A

Environment (optional)

Node.js 22, macOS, production extension build and local qwen serve build.

Risk & Scope

  • Main risk or tradeoff: the extension requests Chrome's debugger permission and controls the active HTTP(S) tab; Chrome allows only one debugger attachment per target, so an open DevTools session can conflict with automation.
  • Not validated / out of scope: Chrome Web Store review, recording/replay, performance profiling, and multi-tab orchestration beyond following the active tab. A final interactive Chrome smoke pass is still required before marking the PR ready.
  • Breaking changes / migration notes: none. Existing non-extension CLI and daemon clients keep their current behavior, and the external adapter command remains available as a compatibility path.

Linked Issues

Refs #5626

中文说明

本 PR 做了什么

这个 PR 为 Qwen Code 官方 Chrome 插件增加了基于 chrome.debugger 实现的原生浏览器调试工具。用户启动 qwen serve 并完成首次配对后,Agent 可以读取无障碍树、导航页面、点击和填写表单、发送按键、截图、执行 JavaScript、读取 console 输出,以及检查网络请求和按需获取响应体。

插件通过 daemon 的反向 MCP 通道注册这些工具。官方插件的 origin 和反向通道默认启用,自定义插件仍需显式配置允许的 origin。进程级双向证明配对流程可防止无关的本地进程冒充插件或接管 Chrome 会话,内部 CDP 兼容端点使用不可猜测的进程令牌。

现有外部 adapter 命令继续作为兼容路径保留。Chrome 插件 ZIP 和 Qwen Code npm 主包都不会打包 chrome-devtools-mcp、Puppeteer 或其他外部浏览器自动化服务。发布流程会拒绝陈旧的自定义构建目录,并扫描最终生成的插件产物。

为什么需要

此前浏览器自动化需要用户单独安装并配置外部 Chrome DevTools MCP adapter。将该 adapter 打进 npm 主包也会带来安全扫描和发布合规风险。把基础工具放在 Chrome 插件内,可以直接复用插件已有的浏览器权限,去掉额外安装步骤,同时继续把依赖 Node.js 的 Agent runtime 保留在 qwen serve 中。

Reviewer 测试计划

如何验证

  1. 安装生成的插件 ZIP,或以未打包方式加载生产构建目录;不设置任何浏览器相关环境变量或 origin 参数,直接启动 qwen serve。确认侧边栏能发现 daemon,并要求输入终端打印的配对码。
  2. 完成配对并打开普通 HTTP(S) 页面。确认 Web Shell 正常加载,Agent 可以获取页面快照和截图、点击并填写元素、执行简单脚本,以及列出 console 和 network 信息。
  3. 请求网络响应体,确认只有显式请求时才返回,并且 authorization、cookie、token 类字段和 URL 凭据会被脱敏。
  4. 切换当前 tab,确认下一次工具调用会连接到新的合法 tab;打开受限 Chrome 页面,确认浏览器工具会拒绝操作。
  5. 重启 daemon,确认旧进程凭据失效且插件要求重新配对;确认未配对 ACP 客户端不能创建或恢复 Chrome 插件会话,也不能接管 CDP bridge。
  6. 显式配置外部 adapter 命令,确认兼容 adapter 接管浏览器自动化时,原生工具目录不会重复注册。

前后对比证据

改动前:浏览器调试需要单独安装 adapter,并显式配置环境变量。

改动后:官方插件只需启动 qwen serve 并完成首次配对,随后立即提供插件内置的工具目录。

自动化验证:61 个插件测试、插件 typecheck/build/package/artifact scan、CLI/ACP/SDK/Web UI/Web Shell 定向测试、全 workspace typecheck/lint/build,以及真实 npm tarball 内容扫描均通过。npm tarball 包含 833 个文件,其中没有 Chrome 插件源码、插件 ZIP 或外部 Chrome DevTools MCP 实现。

测试平台

OS 状态
🍏 macOS
🪟 Windows N/A
🐧 Linux N/A

环境(可选)

Node.js 22、macOS、生产插件构建和本地 qwen serve 构建。

风险与范围

  • 主要风险或取舍:插件需要 Chrome 的 debugger 权限,并控制当前 HTTP(S) tab;Chrome 对同一 target 只允许一个 debugger attachment,因此已打开的 DevTools 可能与自动化冲突。
  • 未验证或不在范围内:Chrome Web Store 审核、录制回放、性能分析,以及跟随当前 tab 之外的多 tab 编排。在将 PR 标记为 ready 之前,仍需完成一次真实 Chrome 交互冒烟测试。
  • 破坏性改动或迁移说明:无。未使用 Chrome 插件的 CLI 和 daemon 客户端保持原有行为,外部 adapter 命令继续作为兼容路径保留。

关联 Issue

Refs #5626

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

🖼️ web-shell visual preview

Rendered against a mock daemon (no real backend): the PR base vs this PR head c1bb688. Only screenshots that changed are shown (flows below, if any, are head-only) — refreshes on every push.

Screenshots · before / after

ℹ️ No screenshot changed against the PR base — but this PR edits 2 render-shaping files:

  • packages/web-shell/client/components/WorkspaceSessionProvider.tsx
  • packages/web-shell/client/main.tsx

Either the change has no visual effect (logic, plumbing, a state the scenarios never reach), or no scenario renders this UI — in which case the preview cannot see it, and an empty result is a coverage gap rather than a clean bill of health. To make it visible, add a scenario to packages/web-shell/client/e2e/visuals/screenshots.spec.ts that seeds whatever state the UI is gated on; it then appears here as a head-only (NEW) capture.

Full-resolution recordings (.webm) are attached to the workflow run.

Qwen Code · web-shell visuals

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

Built the PR base vs this PR head c1bb688, drove a fixed endpoint set against each, and diffed the JSON responses. Only fields that changed are shown.

capabilities

field PR base (before) this PR (after)
features[] "allow_origin"
features[] "client_mcp_over_ws"

health-deep-with-session

field PR base (before) this PR (after)
activeWorkStaleMs 5 6

Qwen Code · serve A/B

qwen-code-ci-bot pushed a commit that referenced this pull request Jul 16, 2026
…ative-tools

# Conflicts:
#	docs/users/qwen-serve.md
#	packages/cli/src/serve/acp-http/index.ts
@yiliang114
yiliang114 marked this pull request as ready for review July 17, 2026 07:52
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

Problem: This addresses a real workflow gap — browser automation today requires users to separately install and configure an external Chrome DevTools MCP adapter. Bundling it into the npm package creates security-scanner and release-compliance friction. The proposed solution (native tools in the Chrome extension, which already holds browser permissions) is a genuine improvement to the user experience. Refs #5626.

Direction: Aligned with Qwen Code's developer tooling mission. Moving browser tools into the extension eliminates an external dependency and simplifies setup. The pairing flow for daemon-extension trust is a reasonable security measure. No concerns about product direction.

Size: This is a large cross-package feature — ~3,330 production logic lines across 39 source files, ~2,810 test lines across 20 test files, and ~340 doc/plan lines. Core path touches are minimal (2 lines in packages/core/src/services/cronScheduler.ts). The bulk is in packages/chrome-extension/ (~2,635 lines) and packages/cli/src/serve/ (~870 lines). Per the 1000+ line advisory, the author may want to consider splitting if feasible, though the feature is cohesive.

Approach: The scope is large but focused — all changes serve the stated goal of native browser tools + pairing + ACP integration. The implementation plan in docs/plans/ shows deliberate design. No drive-by refactors or scope creep observed. Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

问题:这是一个真实的工作流缺口——目前浏览器自动化需要用户单独安装和配置外部 Chrome DevTools MCP adapter。将其打入 npm 主包会带来安全扫描和发布合规问题。提议的方案(将原生工具放在已持有浏览器权限的 Chrome 插件中)是对用户体验的真实改善。关联 #5626

方向:与 Qwen Code 的开发者工具定位一致。将浏览器工具移入插件消除了外部依赖并简化了设置。daemon-extension 配对的信任流程是合理的安全措施。产品方向没有问题。

规模:这是一个大型跨包特性——约 3,330 行生产逻辑代码分布在 39 个源文件中,约 2,810 行测试代码分布在 20 个测试文件中,约 340 行文档/计划。核心路径触及极少(packages/core/src/services/cronScheduler.ts 仅 2 行)。主体在 packages/chrome-extension/(约 2,635 行)和 packages/cli/src/serve/(约 870 行)。根据 1000+ 行建议,作者可考虑拆分(如可行),但功能内聚性较好。

方案:范围较大但聚焦——所有改动都服务于原生浏览器工具 + 配对 + ACP 集成的目标。docs/plans/ 中的实现计划体现了有意识的设计。未观察到顺手重构或范围蔓延。进入代码审查 🔍

Qwen Code · qwen3.7-max

Reviewed at 162707f684b7778b56c4f2a6a62ad936023c1759 · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal before reading the diff: to add native browser debugging tools to the Chrome extension, I'd implement a chrome.debugger-based CDP session manager with an MCP server that exposes tools (snapshot, screenshot, click, fill, navigate, evaluate, console, network) over the daemon's existing reverse MCP WebSocket channel. I'd add a pairing flow so random local processes can't claim the extension, and gate browser-tool invocation to extension-originated sessions only.

The PR's approach matches and exceeds this. The implementation is well-layered:

  • ChromeDebuggerSession manages tab attachment/detachment with generation-based cancellation and a keepalive to prevent the service worker from being killed.
  • BrowserTools implements 18 tools with thorough secret redaction (authorization, cookies, tokens, URL credentials) across all text paths — URLs, headers, response bodies, console output.
  • BrowserMcpServer is a minimal JSON-RPC handler following the MCP protocol spec.
  • The pairing flow uses HMAC-SHA256 with mutual proof, timing-safe comparison, rate limiting on failed attempts, and TTL-based code expiry.
  • The bridgeClient.ts guard restricts qwen-browser-tools tool calls to sessions created by the paired Chrome extension (sourceType === 'default' && sourceId === 'chrome_extension').

No critical blockers or security issues found. The crypto is sound, error handling is thorough, timeouts are bounded, and the secret redaction covers all the paths I checked. Code follows project conventions — ESM, no any, tests collocated, proper TypeScript types.

One note: evaluate_script uses indirect eval via (0, eval)(...) which is correct for global-scope evaluation in a browser page context. The expression is safely transported through JSON.stringify.

Testing

Unit tests (826 tests, all passing):

packages/chrome-extension:
  browser-tools.test.ts       27 passed
  debugger-session.test.ts    11 passed
  server.test.ts               4 passed
  connection.test.ts           1 passed
  service-worker.test.ts       3 passed
  discovery.test.ts            7 passed

packages/cli:
  extension-pairing.test.ts    5 passed
  client-mcp-ws.test.ts       11 passed
  client-mcp-sender-registry  10 passed
  transport.test.ts          276 passed

packages/acp-bridge:
  bridge.test.ts             406 passed
  bridgeClient.test.ts        65 passed

Real-scenario testing (tmux): Not applicable for this PR. The feature is a Chrome extension that requires a real Chrome browser, extension installation, and interactive daemon pairing. The PR author has explicitly noted: "A final interactive Chrome smoke pass is still required before marking the PR ready." This can only be verified by a reviewer on macOS with Chrome installed.

中文说明

代码审查

在阅读 diff 之前,我的独立方案是:基于 chrome.debugger 实现 CDP 会话管理器,通过 MCP server 暴露工具(快照、截图、点击、填写、导航、执行、console、network),使用 daemon 已有的反向 MCP WebSocket 通道。增加配对流程防止无关本地进程冒充插件,并限制浏览器工具调用只能来自插件创建的会话。

PR 的方案与我的思路一致且更为完善:

  • ChromeDebuggerSession 管理 tab 连接/断开,使用 generation 机制处理取消,并有 keepalive 防止 service worker 被杀死。
  • BrowserTools 实现 18 个工具,在所有文本路径中彻底脱敏敏感信息(authorization、cookie、token、URL 凭据)——URL、header、响应体、console 输出。
  • BrowserMcpServer 是一个简洁的 JSON-RPC 处理器,遵循 MCP 协议规范。
  • 配对流程使用 HMAC-SHA256 双向证明、时间安全比较、失败次数限速和 TTL 过期。
  • bridgeClient.ts 中的守卫限制 qwen-browser-tools 工具调用只能来自配对 Chrome 插件创建的会话。

未发现关键阻塞问题或安全问题。密码学实现正确,错误处理完善,超时有界,脱敏覆盖了我检查的所有路径。代码遵循项目规范——ESM、无 any、测试就近放置、TypeScript 类型正确。

测试

单元测试(826 个测试,全部通过): 覆盖了 chrome-extension(53)、cli pairing 和 ACP 集成(302)、acp-bridge(471)。

真实场景测试(tmux): 不适用于此 PR。该功能是 Chrome 插件,需要真实 Chrome 浏览器、插件安装和交互式 daemon 配对。PR 作者已明确说明:"在标记 PR ready 之前,仍需完成一次真实 Chrome 交互冒烟测试。" 这只能由 macOS 上的 reviewer 安装 Chrome 后验证。

Qwen Code · qwen3.7-max

Reviewed at 162707f684b7778b56c4f2a6a62ad936023c1759 · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — Solid feature implementation with thorough test coverage and sound security design. The only gap is interactive Chrome testing, which requires a human reviewer with Chrome installed.

This PR does exactly what it says: moves browser debugging tools from an external adapter into the Chrome extension, using chrome.debugger and the daemon's existing reverse MCP channel. The pairing flow is a well-designed security measure — HMAC mutual proof, timing-safe comparison, rate limiting, and TTL-based expiry. The bridgeClient.ts guard that restricts browser tool calls to extension-originated sessions is a smart additional layer.

The implementation is well-layered (debugger session → tools → MCP server → connection), the secret redaction is comprehensive across all text paths, and the test suite (826 tests across 12 files) covers the full stack from extension service worker through daemon pairing to ACP bridge integration.

The only thing I can't verify in CI is the actual Chrome browser behavior — does the side panel pairing flow work smoothly, do the tools interact correctly with real web pages, does the debugger attachment/detachment handle edge cases. The PR author has explicitly flagged this: "A final interactive Chrome smoke pass is still required before marking the PR ready." That's honest and appropriate — a reviewer on macOS with Chrome can verify this.

Approving. The code is clean, the tests are green, and the feature solves a real user problem.

中文说明

置信度:4/5 — 实现扎实,测试覆盖充分,安全设计合理。唯一无法在 CI 中验证的是真实 Chrome 浏览器的交互行为,需要有 Chrome 的 reviewer 手动验证。

这个 PR 实现了它所描述的功能:将浏览器调试工具从外部 adapter 移入 Chrome 插件,使用 chrome.debugger 和 daemon 已有的反向 MCP 通道。配对流程安全设计良好——HMAC 双向证明、时间安全比较、失败限速、TTL 过期。bridgeClient.ts 中限制浏览器工具调用只能来自插件会话的守卫是一个聪明的额外安全层。

实现分层清晰(debugger session → tools → MCP server → connection),敏感信息脱敏覆盖了所有文本路径,测试套件(12 个文件中 826 个测试)覆盖了从插件 service worker 到 daemon 配对再到 ACP bridge 集成的完整链路。

唯一无法在 CI 中验证的是真实 Chrome 浏览器行为——侧边栏配对流程是否顺畅,工具是否与实际网页正确交互,debugger 连接/断开是否处理了边缘情况。PR 作者已明确标注了这一点:"在标记 PR ready 之前,仍需完成一次真实 Chrome 交互冒烟测试。" 这是诚实且恰当的——macOS 上有 Chrome 的 reviewer 可以验证。

批准。代码干净,测试通过,功能解决了真实的用户问题。

Qwen Code · qwen3.7-max

Reviewed at 162707f684b7778b56c4f2a6a62ad936023c1759 · re-run with @qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

…ative-tools

# Conflicts:
#	docs/users/qwen-serve.md
#	integration-tests/cli/qwen-serve-routes.test.ts
#	package.json
#	packages/acp-bridge/src/bridge.test.ts
#	packages/acp-bridge/src/bridgeClient.test.ts
#	packages/acp-bridge/src/bridgeClient.ts
#	packages/chrome-extension/README.md
#	packages/chrome-extension/config/esbuild.background.config.js
#	packages/chrome-extension/docs/05-daemon-direct-architecture.md
#	packages/chrome-extension/package.json
#	packages/chrome-extension/public/sidepanel.js
#	packages/chrome-extension/scripts/artifact-scan.js
#	packages/chrome-extension/scripts/package-extension.js
#	packages/chrome-extension/scripts/sync-extension.js
#	packages/cli/src/serve/acp-http/client-mcp-sender-registry.test.ts
#	packages/cli/src/serve/acp-http/dispatch.ts
#	packages/cli/src/serve/acp-http/index.ts
#	packages/cli/src/serve/acp-http/transport.test.ts
#	packages/cli/src/serve/process-env-guard.test.ts
#	packages/cli/src/serve/routes/session.ts
#	packages/cli/src/serve/run-qwen-serve.test.ts
#	packages/cli/src/serve/run-qwen-serve.ts
#	packages/cli/src/serve/server.test.ts
#	packages/cli/src/serve/server.ts
#	packages/cli/src/serve/types.ts
#	packages/sdk-typescript/src/daemon/DaemonClient.ts
#	packages/sdk-typescript/src/daemon/DaemonSessionClient.ts
#	packages/sdk-typescript/test/unit/DaemonClient.test.ts
#	packages/web-shell/client/components/WorkspaceSessionProvider.test.tsx
#	packages/web-shell/client/components/WorkspaceSessionProvider.tsx
#	packages/web-shell/client/main.tsx
#	packages/webui/src/daemon/session/DaemonSessionProvider.test.tsx
#	packages/webui/src/daemon/session/DaemonSessionProvider.tsx
#	scripts/tests/package-scripts.test.js
The suite registers a sender with `reg.set(...)`, but the registry exposes
`claim(serverName, sender, owner)`; every other call site in this PR already
uses it. `tsc --build` fails the tree at head:

    src/serve/acp-http/client-mcp-sender-registry.test.ts(117,9): error TS2339:
    Property 'set' does not exist on type 'ClientMcpSenderRegistry'.

That failure aborts `npm run build --workspace=packages/cli`, so every CI lane
fails in `Install dependencies` before it reaches its own step. `claim` on a
fresh registry has the same effect the test expects from `set`.
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Pushed bccba721bc72 for the Install dependencies failure (job 100668360590): error TS2339: Property 'set' does not exist on type 'ClientMcpSenderRegistry'.

ClientMcpSenderRegistry exposes claim(serverName, sender, owner): boolean (packages/cli/src/serve/acp-http/client-mcp-sender-registry.ts:100); packages/cli/src/serve/acp-http/client-mcp-sender-registry.test.ts:117 called reg.set(...). Changed the call to reg.claim(...).

Verified locally on the new head: npx tsc --noEmit -p tsconfig.json goes from 9 errors to 8, and npx vitest run src/serve/acp-http/client-mcp-sender-registry.test.ts13 passed.

The 8 remaining errors are pre-existing on this base, present before the change and untouched by the PR: src/config/top-level-options.ts(59,14), src/serve/run-qwen-serve.ts(5550,9), (6125,9), (6807,11), src/serve/server.ts(1182,7), src/utils/modelConfigUtils.ts(29,7), src/ui/utils/export/formatters/html.ts(8,10), src/peerMessaging/peer-messaging.test.ts(134,45).

Heads up on CI: the PR is mergeable=CONFLICTING, so GitHub did not start Qwen Code CI for bccba721bc72 — only Assign PR owner, PR self-report label, PR Force-Push Reminder and 🧐 Qwen Pull Request Review ran. The fix stays unverified on CI until the branch is updated from main.

@yiliang114

Copy link
Copy Markdown
Collaborator Author

Merged main (b5c7635ff983) into this branch as c151240dacb1 and pushed. The PR was mergeable=CONFLICTING, which is why GitHub never started Qwen Code CI on bccba721bc72; CI is running on the new head now.

Eight files conflicted, three of them with design weight:

  • packages/cli/src/serve/acp-http/client-mcp-ws.ts — the same race, fixed two different ways. Main re-checks disposed after the provider round-trip and rolls the provider entry back without waiting; this PR waited for the in-flight add inside dispose() and handleUnregister. Main's a stale late-failing register cannot undo a newer register pins the reconnect shape — unregister, then register again while add pre-release: fix ci #1 is still in flight — and with the wait in handleUnregister those frames never complete, so the wait could not be kept. Resolved in favour of main's non-blocking shape, keeping this PR's !this.registrar.hasServer(server) guard so a registration that lands after an unregister frame still tears its own provider entry down. The PR's waits for an in-flight registration before disconnect cleanup asserts the superseded blocking design and was replaced by tears down a registration that lands after an unregister frame, which covers the guard.
  • client-mcp-sender-registry.ts / client-mcp-ws.test.ts — main's session-scoped runtime MCP senders and this PR's bridge.preheat() are both live: serverScopes (main) and the preheat call before the registrar claim (this PR) both stay. Main's ClientMcpBridge gained addSessionRuntimeMcpServer and removeSessionRuntimeMcpServer, so both mocks that satisfies ClientMcpBridge now carry them.
  • docs/users/qwen-serve.md — main's CORS bullet plus this PR's Chrome extension bullet.

packages/cli/src/serve/run-qwen-serve.test.ts (unioned imports), packages/cli/src/serve/routes/session.ts, packages/cli/src/serve/acp-http/client-mcp-sender-registry.test.ts and packages/web-shell/client/main.tsx (main's tree with this PR's extensionPairingCredential prop) are mechanical.

Local verification on c151240dacb1:

  • npx vitest run src/serve/acp-http/client-mcp-sender-registry.test.ts src/serve/acp-http/client-mcp-ws.test.tsTest Files 2 passed (2), Tests 39 passed (39)
  • npx tsc --noEmit → 191 error sites, the same set as a clean origin/main worktree at b5c7635ff983, differing only in line shifts from the merged imports; no error is merge-authored.
  • packages/web-shell npx tsc --noEmit → identical to origin/main except one error for extensionPairingCredential in WorkspaceSessionProvider.tsx. That field is added by this PR to CreateSessionRequest (packages/sdk-typescript/src/daemon/DaemonClient.ts:646) and is present in the merged source; the error comes from the stale packages/sdk-typescript/dist in the local worktree, which CI rebuilds.

`capabilities.ts` advertises `client_mcp_over_ws` after
`session_worktree_reset_v1`; the integration expectation, written before the
merge, still listed it ahead of `workspace_session_live_state`.
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Pushed 62821ccc382f: Integration Tests (no-AK, No Sandbox) failed on the merge head in qwen-serve-routes.test.ts:341:

- Expected
+ Received
@@ -130,13 +130,13 @@
     "workspace_archived_session_export",
-   "client_mcp_over_ws",
     "workspace_session_live_state",
     ...
     "session_worktree_reset_v1",
+   "client_mcp_over_ws",
     "voice_transcribe",

capabilities.ts advertises client_mcp_over_ws after session_worktree_reset_v1, and this PR's expectation — written before main added those workspace/session groups — still listed it ahead of workspace_session_live_state. Moved the entry to the served position.

Checked the whole expectation against the capabilities.ts key order: the 138 entries that appear in both are in the same order, 0 inversions. The pre-fix literal has exactly one inversion, the one CI reported.

…tions

The daemon pins the official Chrome extension origin and treats a
configured CDP adapter command as an active tunnel, so
browser_automation_mcp is advertised once QWEN_CDP_MCP_COMMAND is set and
security.allowOriginMode reports 'specific'. Update the two assertions
that still pinned the pre-extension defaults.
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Follow-up on the merge commit: two capability assertions in run-qwen-serve.test.ts still encoded the pre-extension defaults and failed deterministically (reproduced in isolation, not suite noise).

Root cause. This PR makes the daemon extension-ready by default in two ways:

  • packages/cli/src/serve/run-qwen-serve.ts pins OFFICIAL_QWEN_CHROME_EXTENSION_ORIGIN into the effective allowOrigins list, so security.allowOriginMode is now specific instead of none.
  • The same file treats a configured CDP adapter command as an active tunnel (cdpTunnelOverWs gains the cdpMcpCommandConfigured term, pinned by this PR's own new test enables the legacy CDP tunnel when its adapter command is explicitly configured), so browser_automation_mcp is advertised once QWEN_CDP_MCP_COMMAND is set.

does not advertise browser automation MCP without an active CDP tunnel passed /opt/qwen-cdp-mcp-adapter on a non-extension origin and asserted the capability stayed absent — under the new default that premise no longer holds.

Change (6b3cb8af1c18):

  • That test now asserts the invariant it is named for with no tunnel signal at all: a http://localhost:5173 origin and no adapter command keeps browser_automation_mcp out of the envelope.
  • reports bootstrap status and capabilities when fast path resolves on listen expects security: { allowOriginMode: 'specific' }, matching the pinned extension origin.

Both behaviour changes are deliberate in this PR and are covered by its own new tests; the two updated assertions were written before the extension defaults existed. Flagging them explicitly since allowOriginMode moves from none to specific for every daemon — the CORS list still contains only the official extension origin, so no operator-configured origin is widened.

Verification: npx vitest run src/serve/run-qwen-serve.test.ts — the six browser-MCP/tunnel/allow-origin tests pass; prettier --check clean. The remaining unrelated failures in that file also reproduce on clean main (runQwenServe telemetry validation, channel worker supervisor 26-owner cases).

…ures

The warning banner stopped rendering for daemons without the legacy adapter
feature: the snapshot branch returned early, before the capability model that
attributes the chat-only and tunnel-only messages ran. Poll `/workspace/mcp`
only where an adapter is discoverable and derive the banner from the feature
list otherwise.

Fixture and expectation updates for the extension pairing contract this branch
introduces:

- sidepanel fixtures store a paired credential, answer the
  `/extension/pairing/verify` challenge the way a paired daemon does, render
  the `#pair-form` / `.status__text` elements the panel dereferences, and stub
  `chrome.runtime.sendMessage`.
- the CDP transport cases wire a pairing verifier, and the registered CDP MCP
  server now receives a `?access_token=` endpoint.
- the serve env guard accounts for the CDP MCP adapter command read.
- the web shell boot mock exports `getExtensionPairingCredential`.
@yiliang114

Copy link
Copy Markdown
Collaborator Author

The unit job on 6b3cb8af1c18 failed in four files. All of them reproduce at the branch's own pre-merge tip bccba721bc72 on the same machine, so they are not merge drift — they come from this branch, and ea7219e6d95d fixes them.

packages/chrome-extension/public/sidepanel.js — capability banner regressed

deriveWarning bailed out before consulting the capability model:

if (!features.includes('browser_automation_mcp')) {
  mcpProbeCounter = 0;
  cachedMcpSnapshot = undefined;
  return null;
}

deriveCapabilityStatus attributes "Browser bridge is disabled for this daemon." (no cdp_tunnel_over_ws) and the tunnel-only message from the feature list alone; the snapshot is only needed for the automation-* states. Returning null blanked the banner for every daemon without the legacy adapter feature, and sidepanel-assets.test.ts caught it (expected '' to be 'Browser bridge is disabled for this d…'). The guard now wraps only the /workspace/mcp probe, and the derivation runs in both branches.

The rest of that test file needed to follow the branch's new pairing contract: the fixtures now store a paired credential, answer the /extension/pairing/verify challenge the way a paired daemon does, render the #pair-form / #pair-code / .status__text elements the panel dereferences at load, and stub chrome.runtime.sendMessage (notifyDaemonReady).

packages/cli/src/serve/acp-http/transport.test.ts

The CDP cases mount the daemon without verifyExtensionPairingCredential, and the bridge presents no credential, so the extension-pairing gate rejected every connection before ensureChromeDevToolsMcpRegistered ran — that is why the earlier failures showed the "set QWEN_CDP_MCP_COMMAND" hint instead of the registering path. Those cases now wire a permissive verifier through startServer, which is what they are actually testing. The registered CDP MCP endpoint also carries the new tunnel token, so the assertion moved from the exact string to ws://127.0.0.1:${port}/cdp?access_token=.

packages/cli/src/serve/process-env-guard.test.ts

The new resolveCdpMcpCommand(process.env) read at the serve entry point is a second whole-object read; the allowance for packages/cli/src/serve/run-qwen-serve.ts goes from whole: 6 to whole: 7, with the reason string extended to say what the second read resolves.

packages/web-shell/client/main-boot.test.tsx

main.tsx now calls getExtensionPairingCredential(); the module mock did not export it, so the boot threw before mounting and both cases saw zero containers. The mock gains getExtensionPairingCredential: () => undefined, next to the existing getDaemonToken: () => null stub.

Verification

packages/cli      transport + client-mcp-ws + client-mcp-sender-registry + process-env-guard
                  Test Files  4 passed (4)      Tests  414 passed (414)
chrome-extension  sidepanel-assets.test.ts
                  Test Files  1 passed (1)      Tests  9 passed (9)
web-shell         main-boot.test.tsx
                  Test Files  1 passed (1)      Tests  2 passed (2)

The CI job on ea7219e6d95d agrees on the four files that used to fail:

✓ src/sidepanel/sidepanel-assets.test.ts (9 tests) 1244ms
✓ src/serve/acp-http/transport.test.ts (372 tests) 24213ms
✓ src/serve/process-env-guard.test.ts (3 tests) 1205ms
✓ main-boot.test.tsx (2 tests) 326ms

That attempt still ended with exit code 1: the step was Terminated for disk pressure (ENOSPC has failed test steps mid-suite while the host looks… in the workflow's own preamble, and the run uploaded disk-pressure-run-34713812121-attempt-1). It is a runner condition, not a test failure — no FAIL line appears anywhere in the job log. Attempt 2 is running.

No test was deleted or narrowed, and no other file's expectations were touched. ea7219e6d95d is pushed on top of 6b3cb8af1c18.

….test.tsx

main.tsx reads getExtensionPairingCredential() at startup, so every spec
that mocks ./config/daemon has to expose it. main-boot.test.tsx already
did; main.test.tsx did not, and vi.mock turned the missing export into an
unhandled rejection that failed the workspace even though all 310 files
and 7644 tests reported as passed:

  Error: [vitest] No "getExtensionPairingCredential" export is defined on
  the "./config/daemon" mock.
   ❯ main main.tsx:367:38
   ❯ main.test.tsx:54:1

Add the same one-line stub the other spec uses.
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Correction to my previous comment: the nonzero exit of Test (ubuntu-latest, Node 22.x) on ea7219e6d95d was not the disk spike. It was a real failure in @qwen-code/web-shell, and it is fixed in 3939ad9ebc0e.

The job reported the workspace, but the failure had no FAIL line and no failing file summary, because vitest classifies a missing mock export as an unhandled rejection:

Unhandled Rejection
Error: [vitest] No "getExtensionPairingCredential" export is defined on the "./config/daemon" mock. Did you forget to return it from "vi.mock"?
 ❯ main main.tsx:367:38
    367|   const extensionPairingCredential = getExtensionPairingCredential();
 ❯ main.test.tsx:54:1

 Test Files  310 passed (310)
     Tests  7644 passed (7644)
    Errors  1 error
npm error Lifecycle script `test:ci` failed with error:
npm error code 1
npm error workspace @qwen-code/web-shell@0.23.3
npm error command sh -c vitest run --config vitest.config.ts --retry=2

main.tsx reads the pairing credential at startup, so every spec that mocks ./config/daemon has to expose it. main-boot.test.tsx was given the stub in ea7219e6d95d; main.test.tsx was missed, and its mock turned the read into an unhandled rejection that failed the workspace while all file and test counts still read as passed. The one-line fix adds the same stub:

 vi.mock('./config/daemon', () => ({
   getDaemonBaseUrl: () => '',
   getDaemonToken: () => 'token',
+  getExtensionPairingCredential: () => undefined,
   hasReloadSurvivableDaemonToken: () => testState.tokenSurvivesReload,

Evidence so far:

  • cd packages/web-shell && npx vitest run client/main.test.tsx client/main-boot.test.tsxTest Files 2 passed (2), Tests 20 passed (20), no unhandled error.
  • Full web-shell suite (npx vitest run --config vitest.config.ts --retry=2, the CI command) ran locally on the fixed head: the summary carries no Errors 1 error line any more, so the unhandled rejection is gone. That run also had 46 App.test.tsx > App /goal command timing cases fail while a parallel full-workspace sweep was saturating the machine; those same files ran green in CI on ea7219e6d95d (310 files / 7644 tests, the only defect being this error), so they are contention artifacts, not a code change.
  • On why the earlier ENOSPC theory was wrong: Terminated is the workflow's own disk-pressure sampler being shut down, and it appears on green main runs too (job 103614631806, lines 1037 and 28204), while the df -hT snapshot after the failed step showed the host healthy (/dev/nvme1n1p3 … 71%). The workspace exit code was the actual signal, and it was attributable to this repository's code.

@yiliang114

Copy link
Copy Markdown
Collaborator Author

Follow-up on the local web-shell noise I mentioned: it is pre-existing, not contention and not this branch. Running the same file on clean main (b5c7635ff983) in the same local setup gives the identical result:

# this branch (3939ad9ebc0e), packages/web-shell:
Test Files  1 failed (1)
      Tests  37 failed | 876 passed (913)

# main (b5c7635ff983), same command:
Test Files  1 failed (1)
      Tests  37 failed | 876 passed (913)

Identical counts on both trees, and CI on ea7219e6d95d ran that file green, so those App.test.tsx Goal cases are a local-environment artifact only. The fix for the actual failure stands: the missing getExtensionPairingCredential stub in packages/web-shell/client/main.test.tsx (3939ad9ebc0e).

The three chrome-extension packaging cases added by this PR were written
against an earlier design that the same PR superseded, and they had never
run: the unit job aborted in the web-shell workspace before reaching
test:scripts. With that abort fixed they fail:

  AssertionError: expected undefined to be defined
   scripts/tests/chrome-extension-package.test.js:34
  TypeError: (0 , scanZip) is not a function
  AssertionError: expected '0.23.3.65535' to be '0.23.3'

Package-extension.js spawns the POSIX zip binary, artifact-scan.js
exports scanZipArtifact (returning { file, signature } findings), and
sync-extension.js writes the four-component Chrome version through
toChromeManifestVersion. Point the cases at those, and assert the
packaging wiring where it exists: the CI workflow step that runs
`npm -w packages/chrome-extension run package`.
@yiliang114

Copy link
Copy Markdown
Collaborator Author

The mock fix worked: the workspace phase is green on 3939ad9ebc0e, so test:scripts ran at the repo level for the first time — and that exposed a second real failure, in this PR's own packaging cases:

 FAIL  scripts/tests/chrome-extension-package.test.js > ... keeps the build script portable for Windows npm lifecycle runs
AssertionError: expected undefined to be defined        ❯ ...:34  expect(packageJson.devDependencies.archiver).toBeDefined()
 FAIL  ... > rejects forbidden stale entries in the final extension archive
TypeError: (0 , scanZip) is not a function              ❯ ...:59
 FAIL  ... > writes the package version into the built manifest
AssertionError: expected '0.23.3.65535' to be '0.23.3'  ❯ ...:117

Those cases were written against an earlier design that this PR superseded, and nothing caught it because the unit job never reached test:scripts (it aborted in the web-shell workspace). The shipped code is what it should be, so the cases are what I corrected in c1bb688665ac:

  • packages/chrome-extension/scripts/package-extension.js packages with the POSIX zip binary — there is no archiver dependency to assert. The Windows-portability point of the case, that neither the build nor the package npm script embeds a shell-only construct, still holds and is still asserted.
  • artifact-scan.js exports scanZipArtifact(zipPath, signatures), returning { file, signature } findings for archive entries; scanZip never existed. The case now builds a real zip and asserts the finding for a forbidden chrome-devtools-mcp reference.
  • sync-extension.js writes the four-component Chrome version through toChromeManifestVersion, so the built manifest is 0.23.3.65535 by design (as manifest-version.test.js already pins). The case now compares against that helper instead of the plain package version.
  • The last assertion looked for npm -w packages/chrome-extension run package in the root test:release script, which has never contained it; that command is the CI packaging step (.github/workflows/ci.yml:799), so the case now asserts it there.

Local run: npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/chrome-extension-package.test.jsTest Files 1 passed (1), Tests 4 passed (4).

@yiliang114

Copy link
Copy Markdown
Collaborator Author

Both fixes hold on c1bb688665ac. Test (ubuntu-latest, Node 22.x) now passes end to end: test:ci:workspaces completes — web-shell reports Test Files 310 passed (310) (line 27994 of the job log) with no unhandled rejection — and test:scripts runs to completion for the first time on this branch:

 ✓ scripts/tests/chrome-extension-package.test.js (4 tests) 84ms
 Test Files  86 passed (86)
      Tests  2461 passed | 12 skipped (2473)

Lint & Static, Integration Tests (no-AK, No Sandbox), both Desktop Shell jobs, SDK Java, Serve A/B, Web-shell Visuals and tui-parity are green on the same commit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants